﻿using System;
using System.Threading;
using log4net;
using OdsServer.Common;
using OdsServer.Common.Packet;
using System.Diagnostics;
using StructureMap;

namespace OdsServer.Device
{
    public sealed class DeviceHandler : IDeviceHandler
    {
        private static readonly ILog log    = LogManager.GetLogger(typeof(DeviceHandler));
        private IDeviceInteropAI deviceAI   = null;
        private Thread deviceThread         = null;
        private DeviceDataCallback callback = null;

        private volatile bool threadQuit    = false;
        private Stopwatch stopwatch         = null;

        private IntPtr parentWindowHandle   = IntPtr.Zero;
        private int deviceReadInterval      = 0;
        private ushort channelBase          = 0;
        private ushort channelCount         = 0;

        public DeviceHandler(IDeviceInteropAI deviceAI)
        {
            this.deviceAI   = deviceAI;
            this.stopwatch  = new Stopwatch();
        }

        ~DeviceHandler()
        {
            this.Stop();
        }

        public DeviceDataCallback ProcessingCallback
        {
            get { return callback; }
            set { callback = value; }
        }

        public bool IsValid
        {
            get
            {
                return (this.parentWindowHandle != IntPtr.Zero
                        && this.deviceReadInterval > 0
                        && this.callback != null );
            }
        }

        public bool IsActive
        {
            get 
            {
                lock (this)
                {
                    return ( this.deviceThread != null
                            && this.deviceThread.IsAlive );
                }
            }
        }

        public SetupParamChangedCallback SetupParamChangedCallback
        {
            get
            {
                return new SetupParamChangedCallback(this.OnSetupParamChanged);
            }
        }

        public void SelectDevice(IntPtr targetWindowHandle)
        {
            this.deviceAI.SelectDevice(targetWindowHandle);
        }

        public void Start()
        {
            if (this.callback == null)
                throw new DeviceException("Processing callback is not set.");

            this.InitDevice();
            this.deviceThread               = new Thread(new ThreadStart(SendReadingData));
            this.deviceThread.IsBackground  = true;
            this.deviceThread.Priority      = ThreadPriority.Normal;

            this.deviceThread.SetApartmentState(ApartmentState.MTA);

            this.deviceThread.Start();
        }

        public void Stop()
        {
            if (this.IsActive)
            {
                this.threadQuit = true;

                this.deviceThread.Join();

                this.deviceAI.CloseDevice();
            }
        }

        private void InitDevice()
        {
            int retryCount = 0;

            while (this.deviceAI.Handle == IntPtr.Zero
                    && retryCount < 2)
            {
                this.deviceAI.SelectDevice(this.parentWindowHandle);
                this.deviceAI.OpenDevice();

                retryCount++;
            }

            if (this.deviceAI.Handle == IntPtr.Zero)
                throw new DeviceException("Device cannot be initialized.");
        }

        private int CalculateMillisecondSpinCount()
        {
            int spinCount = Int32.MaxValue / 10;
            int[] reading = new int[3];

            // perform garbage collecting
            GC.Collect();

            // force single core execution
            Thread.BeginThreadAffinity();

            lock (this.stopwatch)
            {
                for (int i = 0; i < 3; i++)
                {
                    // start henchmark with new OS time slice
                    Thread.Sleep(0);

                    this.stopwatch.Reset();
                    this.stopwatch.Start();

                    Thread.SpinWait(spinCount);

                    this.stopwatch.Stop();

                    reading[i] = (int)this.stopwatch.ElapsedMilliseconds;

                    log.DebugFormat("SpinWait {1} in {0} ms", this.stopwatch.ElapsedMilliseconds, i);
                }
            }

            Thread.EndThreadAffinity();

            int avgReading = (reading[0] + reading[1] + reading[2]) / 3;

            log.DebugFormat("Average SpinWait reading: {0} ms", avgReading);

            return (spinCount / avgReading);
        }

        private void SendReadingData()
        {
            int spinCount       = CalculateMillisecondSpinCount();
            int readInterval    = spinCount * this.deviceReadInterval;
            Stopwatch timeLog   = new Stopwatch();

            timeLog.Reset();
            timeLog.Start();

            log.Debug("Starting data transfer...");

            // force single core execution
            Thread.BeginThreadAffinity();

            while (!this.threadQuit)
            {
                lock (this.stopwatch)
                {
                    this.stopwatch.Reset();
                    this.stopwatch.Start();

                    if (callback != null)
                    {
                        //SingleChannelDataPacket testData = new SingleChannelDataPacket((float)rnd.NextDouble(), (ushort)rnd.Next());
                        ushort[] val = new ushort[8];

                        for (ushort i = 0; i < channelCount; i++)
                            this.deviceAI.ReadBinary((ushort)(channelBase + i), out val[i]);

                        MultiChannel8DataPacket testData = new MultiChannel8DataPacket(timeLog.ElapsedTicks, val);

                        callback(testData);
                    }
                    Thread.SpinWait(readInterval);

                    this.stopwatch.Stop();

                    //log.DebugFormat("Data sent in {0} ms", this.stopwatch.ElapsedMilliseconds);
                }
            }

            timeLog.Stop();

            Thread.EndThreadAffinity();
        }

        private void Init(SetupParameters parameters)
        {
            this.deviceReadInterval = parameters.timeInterval;
            this.parentWindowHandle = parameters.parentWindowHanlde;
            this.channelBase        = parameters.baseChannelNumber;
            this.channelCount       = parameters.channelCount;
        }

        private void OnSetupParamChanged(SetupParameters parameters)
        {
            lock (this)
            {
                if (this.IsActive)
                {
                    this.Stop();
                
                    this.Init(parameters);

                    this.Start();
                }
                else
                    this.Init(parameters);
            }
        }
    }
}